package zserio.runtime.io;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;

import zserio.runtime.ZserioError;

/**
 * Convenience class for reading and writing Zserio objects from and to byte arrays.
 * <p>
 * Usage:
 * <pre>
 * MyType myType1;
 * byte[] blob = ZserioIO.write(myType1);
 *
 * MyType myType2 = ZserioIO.read(MyType.class, blob);
 * </pre>
 */
public final class ZserioIO
{
    /**
     * Hides the constructor of the utility class.
     */
    private ZserioIO()
    {
        throw new UnsupportedOperationException("ZserioIO: Private constructor is unsupported.");
    }

    /**
     * Takes a Zserio object, writes it to a byte array stream and returns the resulting byte array.
     *
     * @param <E> Zserio class generated by Zserio.
     * @param obj Zserio object of the given class.
     *
     * @return Byte array with a serialized version of the given object.
     */
    public static <E extends Writer> byte[] write(final E obj) throws ZserioError
    {
        try
        {
            final ByteArrayBitStreamWriter writer = new ByteArrayBitStreamWriter();
            obj.write(writer);
            writer.close();
            return writer.toByteArray();
        }
        catch (final IOException exc)
        {
            throw new ZserioError("ZserioIO: " + exc);
        }
    }

    /**
     * A "virtual constructor", building a Zserio object of a given class, reading from a byte array.
     *
     * @param <E>       Zserio class generated by Zserio.
     * @param clazz     The Class instance of the given class.
     * @param byteArray Byte array to be read.
     *
     * @return Zserio object of the given class.
     */
    public static <E> E read(final Class<E> clazz, final byte[] byteArray) throws ZserioError
    {
        try
        {
            final ByteArrayBitStreamReader reader = new ByteArrayBitStreamReader(byteArray);
            final Constructor<E> constructor = clazz.getConstructor(BitStreamReader.class);
            return constructor.newInstance(reader);
        }
        catch (final SecurityException exc)
        {
            throw new ZserioError("ZserioIO: " + exc);
        }
        catch (final NoSuchMethodException exc)
        {
            throw new ZserioError("ZserioIO: " + exc);
        }
        catch (final IllegalArgumentException exc)
        {
            throw new ZserioError("ZserioIO: " + exc);
        }
        catch (final InstantiationException exc)
        {
            throw new ZserioError("ZserioIO: " + exc);
        }
        catch (final IllegalAccessException exc)
        {
            throw new ZserioError("ZserioIO: " + exc);
        }
        catch (final InvocationTargetException exc)
        {
            throw new ZserioError("ZserioIO: " + exc);
        }
    }

    /**
     * A "virtual constructor", building a Zserio object of a given class, reading from a byte array and
     * passing additional arguments to the Zserio object constructor.
     *
     * @param <E>       Zserio class generated by Zserio.
     * @param clazz     Class instance of the given class.
     * @param byteArray Byte array to be read.
     * @param args      Additional constructor arguments.
     *
     * @return Zserio Read object of the given class.
     */
    public static <E> E read(final Class<E> clazz, final byte[] byteArray, final Object... args)
            throws ZserioError
    {
        try
        {
            final ByteArrayBitStreamReader reader = new ByteArrayBitStreamReader(byteArray);

            // build argument array
            final ArrayList<Object> argList = new ArrayList<Object>();
            argList.add(reader);
            for (final Object arg : args)
            {
                argList.add(arg);
            }

            // find a matching constructor for these arguments
            final Constructor<E> constructor = findConstructor(clazz, argList);
            if (constructor == null)
                throw new ZserioError("ZserioIO: No matching constructor found.");

            Object[] argArray = new Object[argList.size()];
            argArray = argList.toArray(argArray);

            return constructor.newInstance(argArray);
        }
        catch (final IllegalArgumentException exc)
        {
            throw new ZserioError("ZserioIO: " + exc);
        }
        catch (final InstantiationException exc)
        {
            throw new ZserioError("ZserioIO: " + exc);
        }
        catch (final IllegalAccessException exc)
        {
            throw new ZserioError("ZserioIO: " + exc);
        }
        catch (final InvocationTargetException exc)
        {
            throw new ZserioError("ZserioIO: " + exc);
        }
    }

    /**
     * Finds a constructor for a given class matching a given argument list. For primitive paramter types,
     * a boxed argument will be regarded as a a match, e.g. an Integer argument will match an int paramter.
     *
     * @param <E>     Class to be constructed.
     * @param clazz   Class instance of this class.
     * @param argList Argument list for constructor.
     *
     * @return Matching constructor, or null if there is no match.
     */
    @SuppressWarnings("unchecked")
    private static <E> Constructor<E> findConstructor(final Class<E> clazz, final List<Object> argList)
    {
        final Constructor<?>[] constructors = clazz.getConstructors();

        // Iterate over constructors and match arguments
        for (final Constructor<?> constr : constructors)
        {
            final Class<?>[] parameterTypes = constr.getParameterTypes();

            // No match if number of arguments is not equal to number of
            // parameters
            if (argList.size() != parameterTypes.length)
            {
                continue;
            }

            // Now try to match each argument
            boolean match = true;
            for (int i = 0; i < argList.size(); i++)
            {
                final Class<?> type = parameterTypes[i];
                final Object arg = argList.get(i);
                if (!isAssignableFrom(type, arg.getClass()))
                {
                    match = false;
                }
            }
            if (match)
            {
                return (Constructor<E>)constr;
            }
        }
        return null;
    }

    /**
     * Checks if left hand class is assignable from right hand class. Primitive types are regarded as assignable
     * from the corresponding boxed type.
     *
     * @param left  Left hand side of assignment.
     * @param right Right hand side of assignment.
     *
     * @return true Iff classes are assignable.
     */
    private static boolean isAssignableFrom(final Class<?> left, final Class<?> right)
    {
        if (left.isPrimitive())
        {
            if (left == Byte.TYPE)
            {
                return right == Byte.class;
            }
            else if (left == Character.TYPE)
            {
                return right == Character.class;
            }
            else if (left == Integer.TYPE)
            {
                return right == Integer.class;
            }
            else if (left == Long.TYPE)
            {
                return right == Long.class;
            }
            else if (left == Short.TYPE)
            {
                return right == Short.class;
            }
            else if (left == Boolean.TYPE)
            {
                return right == Boolean.class;
            }
            else if (left == Float.TYPE)
            {
                return right == Float.class;
            }
            else if (left == Double.TYPE)
            {
                return right == Double.class;
            }
            else
            {
                return false;
            }
        }
        return left.isAssignableFrom(right);
    }
}
